"""RDFshow templates, template elements and their rendering routines.

Each template is a tree of elements--descendants of the `Elem` class. These
objects correspond to the ``<@...@>`` elements in the template source code. At
the root of the tree is a `Template` instance. Each element class overrides the
`Elem.render` method to do the actual rendering according to the element's
semantics.
"""

import hashlib

import rdflib

import util

# This module is private to the RDFshow library.
__all__ = []

CURRENT_NODE = '@node'
"""Placeholder value for indicating "current node" in statement selections."""


class Context(object):
    """Rendering context corresponding to one element.
    
    Every template element is rendered in a specific context, which includes
    the current (under processing) RDF node set, node, statement set and
    statement, as well as the current template set (which is necessary for
    elements like ``render-node``).
    """
    def __init__(self, node_set, node, stmt_set, stmt, tpl_set):
        """Create a new context with specified contents."""
        self.node_set = node_set
        self.node = node
        self.stmt_set = stmt_set
        self.stmt = stmt
        self.tpl_set = tpl_set


class Elem(object):
    """Base class for all template elements."""
    def __init__(self, children):
        """Create new element with the specified list of `children`."""
        self.children = children
    
    def render(self, context, graph, output):
        """Render the element.
        
        Render the element in `context`, using `graph` as the RDF graph
        containing all descriptions. Write results of rendering to the `output`
        file.
        """
        for child in self.children:
            child.render(context, graph, output)


class Template(Elem):
    """An RDFshow template, simultaneously the root of its element tree."""
    
    # Template type constants.
    NODE_SET, NODE, STMT_SET, STMT = range(4)
    
    def __init__(self, children, type_, flavour, for_nodes=None):
        """Create a new template element. Extends `Elem.__init__`.
        
        Create a new template with the specified list of `children`, of type
        `type_`, flavour `flavour`, for a list of RDFLib nodes `for_nodes` (if
        the type is `NODE` or `STMT`).
        """
        Elem.__init__(self, children)
        self.type = type_
        self.flavour = flavour
        self.for_nodes = for_nodes


class TextElem(Elem):
    """An implied, unprocessed text element."""
    def __init__(self, text):
        """Create a new text element with `text`. Extends `Elem.__init__`."""
        Elem.__init__(self, children=[])
        self.text = text
    
    def render(self, context, graph, output):
        output.write(self.text)


class ForNodeElem(Elem):
    """``for-node`` element."""
    def render(self, context, graph, output):
        if context.node_set is None:
            return
        for node in context.node_set:
            # Replace the context node
            new_context = Context(node_set=context.node_set,
                                  node=node, stmt_set=context.stmt_set,
                                  stmt=context.stmt, tpl_set=context.tpl_set)
            for child in self.children:
                child.render(new_context, graph, output)


class NodeIdElem(Elem):
    """``node-id`` element."""
    def __init__(self, escape):
        """Create a new ``node-id`` element. Extends `Elem.__init__`.
        
        Create a new ``node-id`` element which HTML-escapes its value if
        `escape` is True (and renders it verbatim otherwise).
        """
        Elem.__init__(self, [])
        self.escape = escape
    
    def render(self, context, graph, output):
        n = context.node
        if (n is not None) and (isinstance(n, rdflib.URIRef) or 
                                isinstance(n, rdflib.BNode)):
            node_id = unicode(n)
            if self.escape:
                output.write(util.html_escape(node_id))
            else:
                output.write(node_id)


class _StmtsElem(Elem):
    """Base class for the ``select-stmts`` and ``filter-stmts`` elements."""
    def __init__(self, children, subjects, predicates, objects, lang, limit,
                 remove):
        """Create a new selector/filter element. Extends `Elem.__init__`."""
        Elem.__init__(self, children)
        self.subjects = subjects
        self.predicates = predicates
        self.objects = objects
        self.lang = lang
        self.limit = limit
        self.remove = remove
    
    def _render(self, context, global_graph, output, src_graph):
        # Helper routine to expand "current node" indicators.
        def expand_node(node):
            if node == CURRENT_NODE:
                return context.node
            else:
                return node
        
        subjects = [expand_node(s) for s in self.subjects]
        predicates = [expand_node(p) for p in self.predicates]
        objects = [expand_node(o) for o in self.objects]
        queries = [(s, p, o)
                   for s in subjects for p in predicates for o in objects]
        
        # Create and fill a graph of selection results.
        new_graph = rdflib.ConjunctiveGraph()
        n = 0
        for q in queries:
            for s, p, o in src_graph.triples(q):
                # Add triple to result graph if its object's lang is allowed.
                if (self.lang is None) or \
                   (isinstance(o, rdflib.Literal) and o.language in self.lang):
                    new_graph.add((s, p, o))
                    # Remove from the context if needed.
                    if self.remove:
                        src_graph.remove((s, p, o))
                    # Check result count limit.
                    if self.limit is not None:
                        n += 1
                        if n >= self.limit: break
            if (self.limit is not None) and (n >= self.limit): break
        
        # The graph of selection results is now complete. Render children.
        new_context = Context(node_set=context.node_set, node=context.node,
                              stmt_set=new_graph, stmt=context.stmt,
                              tpl_set=context.tpl_set)
        for child in self.children:
            child.render(new_context, global_graph, output)


class SelectStmtsElem(_StmtsElem):
    """``select-stmts`` element."""
    def __init__(self, children, subjects, predicates, objects, lang, limit):
        """Create a ``select-stmts`` element. Extends `_StmtsElem.__init__`.
        
        Create a new statement selector for the specified `subjects`,
        `predicates` and `objects`. Each of these is a list that either
        contains a single None value if any subject/predicate/object is to be
        matched; or contains all the nodes that will be matched. In place of
        an RDFLib Node object, a special value `CURRENT_NODE` can be placed in
        the lists; it will match the current context node. If `lang` is not
        None, only those statements will be matched whose object is a literal
        with a language code from that list (e.g. ``lang=['en', 'ru']``). If
        `limit` is not None, it should be a non-negative integer indicating the
        maximum number of statements to be selected.
        """
        _StmtsElem.__init__(self, children, subjects, predicates, objects,
                            lang, limit, remove=False)
    
    def render(self, context, graph, output):
        self._render(context, graph, output, src_graph=graph)


class FilterStmtsElem(_StmtsElem):
    """``filter-stmts`` element."""
    def __init__(self, children, subjects, predicates, objects, lang, limit,
                 remove=False):
        """Create a ``filter-stmts`` element. Extends `_StmtsElem.__init__`.
        
        Create a new statement filter for the specified `subjects`,
        `predicates` and `objects`. Each of these is a list that either
        contains a single None value if any subject/predicate/object is to be
        matched; or contains all the nodes that will be matched. In place of
        an RDFLib Node object, a special value `CURRENT_NODE` can be placed in
        the lists; it will match the current context node. If `lang` is not
        None, only those statements will be matched whose object is a literal
        with a language code from that list (e.g. ``lang=['en', 'ru']``). If
        `limit` is not None, it should be a non-negative integer indicating the
        maximum number of statements to be selected. If `remove` is True,
        every matched statement will be removed from the context graph,
        affecting whatever other parts of the templates are using the same
        graph.
        """
        _StmtsElem.__init__(self, children, subjects, predicates, objects,
                            lang, limit, remove)
    
    def render(self, context, graph, output):
        self._render(context, graph, output, src_graph=context.stmt_set)


class ForStmtElem(Elem):
    """``for-stmt`` element."""
    def render(self, context, graph, output):
        if context.stmt_set is None:
            return
        for stmt in context.stmt_set:
            # Replace the context statement
            new_context = Context(node_set=context.node_set,
                                  node=context.node, stmt_set=context.stmt_set,
                                  stmt=stmt, tpl_set=context.tpl_set)
            for child in self.children:
                child.render(new_context, graph, output)


class LiteralValElem(Elem):
    """``literal-val`` element."""
    def __init__(self, escape):
        """Create a new ``literal-val`` element. Extends `Elem.__init__`."""
        Elem.__init__(self, [])
        self.escape = escape
    
    def render(self, context, graph, output):
        if not isinstance(context.node, rdflib.Literal):
            return
        if self.escape:
            output.write(util.html_escape(unicode(context.node)))
        else:
            output.write(unicode(context.node))


class _TriplePartElem(Elem):
    """Base class for ``subject``, ``predicate`` and ``object`` elements."""
    def render(self, context, graph, output, i):
        # i is the index in the triple (0 for subject, ...)
        new_context = Context(node_set=context.node_set,
                              node=context.stmt[i],
                              stmt_set=context.stmt_set, stmt=context.stmt,
                              tpl_set=context.tpl_set)
        for child in self.children:
            child.render(new_context, graph, output)


class SubjectElem(_TriplePartElem):
    """``subject`` element."""
    def render(self, context, graph, output):
        _TriplePartElem.render(self, context, graph, output, 0)

class PredicateElem(_TriplePartElem):
    """``predicate`` element."""
    def render(self, context, graph, output):
        _TriplePartElem.render(self, context, graph, output, 1)

class ObjectElem(_TriplePartElem):
    """``object`` element."""
    def render(self, context, graph, output):
        _TriplePartElem.render(self, context, graph, output, 2)


class NodeCountElem(Elem):
    """``node-count`` element."""
    def __init__(self):
        """Create a new ``node-count`` element. Extends `Elem.__init__`."""
        Elem.__init__(self, [])
    
    def render(self, context, graph, output):
        if context.node_set is not None:
            output.write(unicode(len(context.node_set)))


class StmtCountElem(Elem):
    """``stmt-count`` element."""
    def __init__(self):
        """Create a new ``stmt-count`` element. Extends `Elem.__init__`."""
        Elem.__init__(self, [])
    
    def render(self, context, graph, output):
        if context.stmt_set is not None:
            output.write(unicode(len(context.stmt_set)))


class SortNodesElem(Elem):
    """``sort-nodes`` element."""
    def __init__(self, children, predicate, asc, lang):
        """Create a new ``sort-nodes`` element. Extends `Elem.__init__`.
        
        Create a new ``sort-nodes`` element that sorts by `predicate` in
        ascending order if `asc` is True, in descending order otherwise.
        If `lang` is not None, consider only those objects which are literals
        with language `lang`.
        """
        Elem.__init__(self, children)
        self.predicate = predicate
        self.asc = asc
        self.lang = lang
    
    def render(self, context, graph, output):
        # Build up a list of nodes with predicate values, suitable for sorting.
        sortable_pairs = []
        unsortable_nodes = []
        for node in context.node_set:
            found = False
            for s, p, o in graph.triples((node, self.predicate, None)):
                if (self.lang is None) or \
                   (hasattr(o, 'language') and (o.language == self.lang)):
                    found = True
                    sortable_pairs.append((node, o))
                    break
            if not found:
                # If we came here, it means no suitable objects for this
                # subject+predicate have been found in the graph.
                unsortable_nodes.append(node)
        sortable_pairs.sort(key=lambda (n,o): o, reverse=(not self.asc))
        sorted_nodes = [n for (n,o) in sortable_pairs]
        new_node_set = sorted_nodes + unsortable_nodes
        new_context = Context(node_set=new_node_set, node=context.node,
                              stmt_set=context.stmt_set, stmt=context.stmt,
                              tpl_set=context.tpl_set)
        for child in self.children:
            child.render(new_context, graph, output)


class HaveNodesElem(Elem):
    """``have-nodes`` element."""
    def render(self, context, graph, output):
        if (context.node_set is not None) and len(context.node_set) > 0:
            for child in self.children:
                child.render(context, graph, output)


class NoNodesElem(Elem):
    """``no-nodes`` element."""
    def render(self, context, graph, output):
        if (context.node_set is None) or len(context.node_set) < 1:
            for child in self.children:
                child.render(context, graph, output)


class RenderNodeSetElem(Elem):
    """``render-node-set`` element."""
    def __init__(self, flavour):
        """Create a ``render-node-set`` element. Extends `Elem.__init__`.
        
        `flavour` is the flavour of the template to be rendered (None for no
        flavour).
        """
        Elem.__init__(self, [])
        self.flavour = flavour
    
    def render(self, context, graph, output):
        context.tpl_set.render_node_set(context.node_set, graph, output,
                                        self.flavour)


class RenderNodeElem(Elem):
    """``render-node`` element."""
    def __init__(self, flavour):
        """Create a new ``render-node`` element. Extends `Elem.__init__`.
        
        `flavour` is the flavour of the template to be rendered (None for no
        flavour).
        """
        Elem.__init__(self, [])
        self.flavour = flavour
    
    def render(self, context, graph, output):
        context.tpl_set.render_node(context.node, graph, output, self.flavour)


class IfNodeResourceElem(Elem):
    """``if-node-resource`` element."""
    def render(self, context, graph, output):
        if isinstance(context.node, rdflib.URIRef):
            for child in self.children:
                child.render(context, graph, output)


class IfNodeBlankElem(Elem):
    """``if-node-blank`` element."""
    def render(self, context, graph, output):
        if isinstance(context.node, rdflib.BNode):
            for child in self.children:
                child.render(context, graph, output)


class IfNodeLiteralElem(Elem):
    """``if-node-literal`` element."""
    def render(self, context, graph, output):
        if isinstance(context.node, rdflib.Literal):
            for child in self.children:
                child.render(context, graph, output)


class NodeIdSha1Elem(Elem):
    """``node-id-sha1`` element."""
    def __init__(self):
        """Create a new ``node-id-sha1`` element. Extends `Elem.__init__`."""
        Elem.__init__(self, [])
    
    def render(self, context, graph, output):
        n = context.node
        if (n is not None) and (isinstance(n, rdflib.URIRef) or
                                isinstance(n, rdflib.BNode)):
            node_id = unicode(context.node)
            sha1 = unicode(hashlib.sha1(node_id).hexdigest())
            output.write(sha1)


class LiteralLangElem(Elem):
    """``literal-lang`` element."""
    def __init__(self):
        """Create a new ``literal-lang`` element. Extends `Elem.__init__`."""
        Elem.__init__(self, [])
    
    def render(self, context, graph, output):
        if isinstance(context.node, rdflib.Literal) and \
           (context.node.language is not None):
            output.write(unicode(context.node.language))


class RenderStmtSetElem(Elem):
    """``render-stmt-set`` element."""
    def __init__(self, flavour):
        """Create a ``render-stmt-set`` element. Extends `Elem.__init__`.
        
        `flavour` is the flavour of the template to be rendered (None for no
        flavour).
        """
        Elem.__init__(self, [])
        self.flavour = flavour
    
    def render(self, context, graph, output):
        context.tpl_set.render_stmt_set(context.stmt_set, graph, output,
                                        self.flavour)


class RenderStmtElem(Elem):
    """``render-stmt`` element."""
    def __init__(self, flavour):
        """Create a new ``render-stmt`` element. Extends `Elem.__init__`.
        
        `flavour` is the flavour of the template to be rendered (None for no
        flavour).
        """
        Elem.__init__(self, [])
        self.flavour = flavour
    
    def render(self, context, graph, output):
        context.tpl_set.render_stmt(context.stmt, graph, output, self.flavour)


class _PartsElem(Elem):
    """Base class for ``subjects``, ``predicates`` and ``objects`` elements."""
    def _render(self, context, graph, output, node_maker_name):
        if context.stmt_set is not None:
            node_maker = getattr(context.stmt_set, node_maker_name)
            # Use set() here to select only one of each node.
            new_node_set = list(set(node_maker()))
            new_context = Context(node_set=new_node_set, node=context.node,
                                  stmt_set=context.stmt_set, stmt=context.stmt,
                                  tpl_set=context.tpl_set)
            for child in self.children:
                child.render(new_context, graph, output)


class SubjectsElem(_PartsElem):
    """``subjects`` element."""
    def render(self, context, graph, output):
        self._render(context, graph, output, 'subjects')


class PredicatesElem(_PartsElem):
    """``predicates`` element."""
    def render(self, context, graph, output):
        self._render(context, graph, output, 'predicates')


class ObjectsElem(_PartsElem):
    """``objects`` element."""
    def render(self, context, graph, output):
        self._render(context, graph, output, 'objects')


class HaveStmtsElem(Elem):
    """``have-stmts`` element."""
    def render(self, context, graph, output):
        if (context.stmt_set is not None) and len(context.stmt_set) > 0:
            for child in self.children:
                child.render(context, graph, output)


class NoStmtsElem(Elem):
    """``no-stmts`` element."""
    def render(self, context, graph, output):
        if (context.stmt_set is None) or len(context.stmt_set) < 1:
            for child in self.children:
                child.render(context, graph, output)


class _StmtElem(Elem):
    """Base class for the ``select-stmt`` and ``filter-stmt`` elements."""
    def __init__(self, children, subject, predicate, object_, lang, remove):
        """Create a new selector/filter element. Extends `Elem.__init__`."""
        Elem.__init__(self, children)
        self.subject = subject
        self.predicate = predicate
        self.object = object_
        self.lang = lang
        self.remove = remove
    
    def _render(self, context, global_graph, output, src_graph):
        # Helper routine to expand "current node" indicators.
        def expand_node(node):
            if node == CURRENT_NODE:
                return context.node
            else:
                return node
                
        query = (expand_node(self.subject),
                 expand_node(self.predicate),
                 expand_node(self.object))
        
        selected = None
        for s, p, o in src_graph.triples(query):
            # Choose this triple if its object's lang is allowed.
            if (self.lang is None) or \
                (isinstance(o, rdflib.Literal) and o.language == self.lang):
                selected = (s, p, o)
                break
        
        if selected is not None:
            # Remove from the context if needed.
            if self.remove:
                src_graph.remove(selected)
            
            # Render children.
            new_context = Context(node_set=context.node_set, node=context.node,
                                  stmt_set=context.stmt_set, stmt=selected,
                                  tpl_set=context.tpl_set)
            for child in self.children:
                child.render(new_context, global_graph, output)


class SelectStmtElem(_StmtElem):
    """``select-stmt`` element."""
    def __init__(self, children, subject, predicate, object_, lang):
        """Create a ``select-stmt`` element. Extends `_StmtElem.__init__`."""
        _StmtElem.__init__(self, children, subject, predicate, object_, lang,
                            remove=False)
    
    def render(self, context, graph, output):
        self._render(context, graph, output, src_graph=graph)


class FilterStmtElem(_StmtElem):
    """``filter-stmt`` element."""
    def __init__(self, children, subject, predicate, object_, lang,
                 remove=False):
        """Create a ``filter-stmt`` element. Extends `_StmtElem.__init__`."""
        _StmtElem.__init__(self, children, subject, predicate, object_,
                            lang, remove)
    
    def render(self, context, graph, output):
        self._render(context, graph, output, src_graph=context.stmt_set)
